package com.socket.server.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.Img;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.socket.core.constant.ChatConstants;
import com.socket.core.constant.ChatProperties;
import com.socket.core.constant.Constants;
import com.socket.core.enums.RedisTree;
import com.socket.core.enums.UserRole;
import com.socket.core.manage.TokenUserManager;
import com.socket.core.model.command.enmus.User;
import com.socket.core.util.RedisClient;
import com.socket.core.util.Wss;
import com.socket.secure.util.AESUtil;
import com.socket.secure.util.Assert;
import com.socket.server.custom.exception.AccountException;
import com.socket.server.custom.exception.BizException;
import com.socket.server.custom.exception.UploadException;
import com.socket.server.custom.publisher.CommandPublisher;
import com.socket.server.custom.storage.ResourceStorage;
import com.socket.server.mapper.SysUserMapper;
import com.socket.server.model.base.BasePo;
import com.socket.server.model.condition.EmailCondition;
import com.socket.server.model.condition.LoginCondition;
import com.socket.server.model.condition.PasswordCondition;
import com.socket.server.model.condition.RegisterCondition;
import com.socket.server.model.enmus.FileType;
import com.socket.server.model.po.ChatRecordFile;
import com.socket.server.model.po.SysLog;
import com.socket.server.model.po.SysUser;
import com.socket.server.model.vo.SysUserVo;
import com.socket.server.request.IPAddrRequest;
import com.socket.server.service.ResourceService;
import com.socket.server.service.SysGroupService;
import com.socket.server.service.SysLogService;
import com.socket.server.service.SysUserService;
import com.socket.server.util.Bcrypt;
import com.socket.server.util.Email;
import com.socket.server.util.ShiroUser;
import com.socket.server.util.servlet.Requests;
import com.socket.server.util.servlet.Responses;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    private final SysGroupService sysGroupService;
    private final ResourceService resourceService;
    private final SysLogService sysLogService;
    private final RedisClient<Object> redisClient;
    private final TokenUserManager tokenUserManager;
    private final CommandPublisher publisher;
    private final ChatProperties properties;
    private final ChatConstants constants;
    private final IPAddrRequest ipAddrRequest;
    private final ResourceStorage storage;
    private final HttpSession session;
    private final Email sender;

    @Override
    public void login(LoginCondition condition) {
        String code = condition.getCode();
        String guid = condition.getUser();
        // 查询相关用户
        LambdaQueryWrapper<SysUser> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(Validator.isEmail(guid) ? SysUser::getEmail : SysUser::getGuid, guid);
        SysUser user = getFirst(wrapper);
        Assert.notNull(user, "找不到指定账号", UnknownAccountException::new);
        // 验证异地登录
        if (StrUtil.isNotEmpty(code)) {
            String key = RedisTree.EMAIL.concat(user.getEmail());
            Object redisCode = redisClient.get(key);
            Assert.equals(redisCode, code, "验证码不正确", AccountException::new);
            Requests.set(Constants.AUHT_OFFSITE_REQUEST);
            redisClient.remove(key);
        }
        // shiro登录
        // TODO 有关自动登录问题后续解决
        SecurityUtils.getSubject().login(new UsernamePasswordToken(user.getGuid(), condition.getPass()));
        // 保存并设置响应头
        String token = tokenUserManager.setToken(user.getGuid(), AESUtil.getAesKey(session), Constants.SESSION_VALID_TIME);
        session.setAttribute(Constants.AUTH_TOKEN, token);
        Responses.setHeader(Constants.AUTH_TOKEN, token);
    }

    @Override
    public void register(RegisterCondition condition) {
        // 验证邮箱
        String key = RedisTree.EMAIL.concat(condition.getEmail());
        Assert.equals(condition.getCode(), redisClient.get(key), "验证码不正确", BizException::new);
        redisClient.remove(key);
        // 注册
        SysUser user = register0(condition);
        // 登录
        this.login(new LoginCondition(user.getGuid(), condition.getPass()));
    }

    public SysUser register0(RegisterCondition condition) {
        SysUser user = new SysUser();
        user.setRole(UserRole.USER.getRole());
        user.setGuid(RandomUtil.randomNumbers(properties.getGenerateGuidLength()));
        user.setName(StrUtil.emptyToDefault(condition.getName(), "用户" + user.getGuid()));
        user.setHeadimgurl(condition.getImgurl());
        user.setEmail(condition.getEmail());
        user.setHash(Bcrypt.digest(condition.getPass()));
        user.setUin(condition.getUin());
        user.setOpenid(condition.getOpenid());
        super.save(user);
        // 加入默认群组
        sysGroupService.joinGroup(constants.getDefaultGroup(), user.getGuid(), true);
        return user;
    }

    @Override
    public String sendEmail(String email) {
        // 检查邮箱与UID
        if (!Validator.isEmail(email)) {
            final String guid = email;
            LambdaQueryWrapper<SysUser> wrapper = Wrappers.lambdaQuery();
            wrapper.eq(SysUser::getGuid, guid);
            SysUser user = getFirst(wrapper);
            Assert.notNull(user, "找不到相关账号信息", BizException::new);
            Assert.isFalse(user.isDeleted(), "该账号已被永久限制登录", BizException::new);
            email = user.getEmail();
            Assert.notEmpty(email, "该账号未绑定邮箱信息", BizException::new);
        }
        // 检查重复发送间隔
        String etk = RedisTree.EMAIL_TEMP.concat(email);
        Assert.isFalse(redisClient.exist(etk), "验证码发送过于频繁", BizException::new);
        redisClient.set(etk, -1, properties.getEmailSendingInterval());
        // 检查发送次数上限
        String elk = RedisTree.EMAIL_LIMIT.concat(email);
        long count = redisClient.incr(elk, 1, properties.getEmailLimitSendingInterval(), TimeUnit.HOURS);
        Assert.isTrue(count <= 3, "该账号验证码每日发送次数已达上限", BizException::new);
        // 发送邮件
        String code = sender.send(email);
        // 保存到Redis
        etk = RedisTree.EMAIL.concat(email);
        redisClient.set(etk, code, properties.getEmailCodeValidTime(), TimeUnit.MINUTES);
        return DesensitizedUtil.email(email);
    }

    @Override
    public boolean updatePassword(PasswordCondition condition) {
        LambdaUpdateWrapper<SysUser> wrapper = Wrappers.lambdaUpdate();
        String email = condition.getEmail();
        SysUser user = ShiroUser.get();
        if (StrUtil.isEmpty(email)) {
            Assert.notNull(user, "请输入邮箱", BizException::new);
            email = user.getEmail();
        }
        String key = RedisTree.EMAIL.concat(email);
        String code = redisClient.get(key);
        Assert.equals(code, condition.getCode(), "邮箱验证码不正确", BizException::new);
        // 通过邮箱修改密码
        wrapper.eq(SysUser::getEmail, email);
        wrapper.set(SysUser::getHash, Bcrypt.digest(condition.getPassword()));
        redisClient.remove(key);
        return super.update(wrapper);
    }

    @Override
    public void updateMaterial(SysUserVo vo) {
        LambdaUpdateWrapper<SysUser> wrapper = Wrappers.lambdaUpdate();
        // 清空无法修改的字段
        vo.setGuid(null);
        vo.setHeadimgurl(null);
        vo.setEmail(null);
        // 若生日不为空 优先使用基于生日的年龄
        Optional.ofNullable(vo.getBirth()).ifPresent(e -> vo.setAge(DateUtil.age(e, new Date())));
        wrapper.eq(SysUser::getGuid, ShiroUser.getUserId());
        SysUser entity = BeanUtil.copyProperties(vo, SysUser.class);
        Assert.isTrue(super.update(entity, wrapper), "修改失败", BizException::new);
        // 推送变动事件
        publisher.pushUserEvent(vo.getName(), User.NAME);
    }

    @Override
    public String updateAvatar(byte[] bytes) {
        Assert.isTrue(bytes.length <= 0x4b000, "图片大小超过限制", UploadException::new);
        // byte[]转图片
        BufferedImage image;
        try {
            image = ImgUtil.toImage(bytes);
        } catch (IllegalArgumentException | IORuntimeException e) {
            throw new UploadException("未能识别的图片格式");
        }
        // 创建图片文件
        int min = Math.min(image.getWidth(), image.getHeight());
        // 按最小宽度裁剪为正方形
        Image cut = ImgUtil.cut(image, new Rectangle(0, 0, min, min));
        Image scale = ImgUtil.scale(cut, 132, 132);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Img.from(scale).setTargetImageType(ImgUtil.IMAGE_TYPE_PNG).write(bos);
        // 图片映射地址
        bytes = bos.toByteArray();
        String hash = Wss.generateHash(bytes);
        String url = storage.upload(FileType.IMAGE, bytes, hash);
        String mapping = resourceService.getMapping(FileType.IMAGE, hash);
        // 保存头像
        LambdaUpdateWrapper<SysUser> wrapper = Wrappers.lambdaUpdate();
        wrapper.eq(SysUser::getGuid, ShiroUser.getUserId());
        wrapper.set(SysUser::getHeadimgurl, mapping);
        Assert.isTrue(super.update(wrapper), "修改失败", BizException::new);
        // 保存文件映射
        resourceService.save(new ChatRecordFile(null, FileType.IMAGE.getKey(), url, hash, bytes.length));
        // 推送变动事件
        publisher.pushUserEvent(mapping, User.HEADIMG);
        return mapping;
    }

    @Override
    public void updateEmail(EmailCondition condition) {
        // 验证原邮箱
        SysUser user = ShiroUser.get();
        String olds = user.getEmail();
        if (StrUtil.isNotEmpty(olds)) {
            String selfcode = redisClient.get(RedisTree.EMAIL.concat(olds));
            // 对比验证码
            Assert.equals(selfcode, condition.getOcode(), "原邮箱验证码不正确", BizException::new);
        }
        // 验证新邮箱
        String news = condition.getEmail();
        LambdaUpdateWrapper<SysUser> wrapper = Wrappers.lambdaUpdate();
        wrapper.eq(SysUser::getEmail, news);
        Assert.isNull(this.getFirst(wrapper), "该邮箱已被其他账号绑定", BizException::new);
        String newcode = redisClient.get(RedisTree.EMAIL.concat(news));
        Assert.equals(newcode, condition.getNcode(), "新邮箱验证码不正确", BizException::new);
        // 更新邮箱
        wrapper.clear();
        wrapper.eq(SysUser::getGuid, user.getGuid());
        wrapper.set(SysUser::getEmail, news);
        Assert.isTrue(super.update(wrapper), "修改失败", BizException::new);
        ShiroUser.set(SysUser::getEmail, news);
    }

    @Override
    public SysUserVo getUserInfo(String guid, boolean hide) {
        LambdaQueryWrapper<SysUser> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(SysUser::getGuid, guid);
        SysUser user = this.getFirst(wrapper);
        Assert.notNull(user, "找不到此用户信息", AccountException::new);
        SysUserVo target = new SysUserVo();
        // 获取ip所属省
        if (!hide) {
            LambdaQueryWrapper<SysLog> wrapper1 = Wrappers.lambdaQuery();
            wrapper1.eq(SysLog::getGuid, guid);
            wrapper1.isNotNull(SysLog::getClientIp);
            wrapper1.orderByDesc(BasePo::getCreateTime);
            SysLog log = sysLogService.getFirst(wrapper1);
            Optional.ofNullable(log).map(SysLog::getClientIp)
                    .filter(StrUtil::isNotEmpty)
                    .map(ipAddrRequest::getProvince)
                    .ifPresent(target::setProvince);
        }
        // copy bean
        BeanUtil.copyProperties(user, target);
        return target;
    }

    @Override
    public UserRole switchRole(String target) {
        LambdaUpdateWrapper<SysUser> wrapper = Wrappers.lambdaUpdate();
        wrapper.eq(SysUser::getGuid, target);
        SysUser user = getFirst(wrapper);
        boolean admin = Wss.isAdmin(user.getRole());
        UserRole role = admin ? UserRole.USER : UserRole.ADMIN;
        wrapper.set(SysUser::getRole, role.toString());
        super.update(wrapper);
        return role;
    }

    @Override
    public void updateAlias(String target, String alias) {
        LambdaUpdateWrapper<SysUser> wrapper = Wrappers.lambdaUpdate();
        wrapper.eq(SysUser::getGuid, target);
        wrapper.set(SysUser::getAlias, alias);
        super.update(wrapper);
    }
}
